原文地址「React 进阶」 React 全部 api 解读+基础实践大全(夯实基础 2 万字总结) - 掘金
很多同学用react
开发的时候,真正用到的React
的api
少之又少,基本停留在Component
,React.memo
等层面,实际react
源码中,暴露出来的方法并不少,只是我们平时很少用。但是React
暴露出这么多api
并非没有用,想要玩转react
,就要明白这些API
究竟是干什么的,应用场景是什么,今天就让我们从react
到 react-dom
,一次性把react
生产环境的暴露api
复习个遍(涵盖 90%+)。
我们把react
,API
,分为组件类 ,工具类 ,**hooks
**,再加上 react-dom
,一共四大方向,分别加以探讨。
为了能让屏幕前的你,更理解api
,我是绞尽脑汁,本文的每一个api
基本都会出一个demo
演示效果,弥补一下天书般的react
文档 😂😂😂,还有就是我对api
基本概念的理解。创作不易,希望屏幕前的你能给笔者赏个赞 ,以此鼓励我继续创作前端硬文。
老规矩,我们带着疑问开始今天的阅读(自测掌握程度 )?
1 react
暴露的api
有哪些,该如何使用?
2 react
提供了哪些自测性能的手段?
3 ref
既然不能用在函数组件中,那么父组件如何控制函数子组件内的state
和方法?
4 createElement
和cloneElement
有什么区别,应用场景是什么?
5 react
内置的children
遍历方法,和数组方法,有什么区别?
6 react
怎么将子元素渲染到父元素之外的指定容器中?
…
我相信读完这篇文章,这些问题全都会迎刃而解?
组件类 组件类,详细分的话有三种类,第一类说白了就是我平时用于继承的基类组件Component
,PureComponent
,还有就是react
提供的内置的组件,比如Fragment
,StrictMode
,另一部分就是高阶组件forwardRef
,memo
等。
Component Component
是class
组件的根基。 类组件一切始于Component
。对于React.Component
使用,我们没有什么好讲的。我们这里重点研究一下react
对Component
做了些什么。
react/src/ReactBaseClasses.js
1 2 3 4 5 6 function Component (props, context, updater ) { this .props = props; this .context = context; this .refs = emptyObject; this .updater = updater || ReactNoopUpdateQueue ; }
这就是Component
函数,其中updater
对象上保存着更新组件的方法。
我们声明的类组件是什么时候以何种形式被实例化的呢?
react-reconciler/src/ReactFiberClassComponent.js
constructClassInstance
1 2 3 4 5 6 7 8 9 10 11 12 13 function constructClassInstance (workInProgress, ctor, props ) { const instance = new ctor (props, context); instance.updater = { isMounted, enqueueSetState ( ) { }, enqueueReplaceState ( ) {}, enqueueForceUpdate ( ) { }, }; }
对于Component
, react
处理逻辑还是很简单的,实例化我们类组件,然后赋值updater
对象,负责组件的更新。然后在组件各个阶段,执行类组件的render
函数,和对应的生命周期函数就可以了。
PureComponent PureComponent
和 Component
用法,差不多一样,唯一不同的是,纯组件PureComponent
会浅比较,props
和state
是否相同,来决定是否重新渲染组件。所以一般用于性能调优 ,减少render 次数。
什么叫做浅比较 ,我这里举个列子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Index extends React.PureComponent { constructor (props ) { super (props); this .state = { data : { name : "alien" , age : 28 , }, }; } handerClick = () => { const { data } = this .state ; data.age ++; this .setState ({ data }); }; render ( ) { const { data } = this .state ; return ( <div className ="box" > <div className ="show" > <div > 你的姓名是: {data.name} </div > <div > 年龄: {data.age}</div > <button onClick ={this.handerClick} > age++</button > </div > </div > ); } }
点击按钮,没有任何反应 ,因为PureComponent
会比较两次data
对象,都指向同一个data
,没有发生改变,所以不更新视图。
解决这个问题很简单,只需要在handerClick
事件中这么写:
1 this .setState ({ data : { ...data } });
浅拷贝 就能根本解决问题。
memo React.memo
和PureComponent
作用类似,可以用作性能优化,React.memo
是高阶组件,函数组件和类组件都可以使用, 和区别PureComponent
是 React.memo
只能对props
的情况确定是否渲染,而PureComponent
是针对props
和state
。
React.memo
接受两个参数,第一个参数原始组件本身,第二个参数,可以根据一次更新中props
是否相同决定原始组件是否重新渲染。是一个返回布尔值,true
证明组件无须重新渲染,false
证明组件需要重新渲染,这个和类组件中的shouldComponentUpdate()
正好相反 。
React.memo: 第二个参数 返回 true
组件不渲染 , 返回 false
组件重新渲染。 shouldComponentUpdate: 返回 true
组件渲染 , 返回 false
组件不渲染。
接下来我们做一个场景,控制组件在仅此一个props
数字变量,一定范围渲染。
例子 🌰:
控制 props
中的 number
:
1 只有 number
更改,组件渲染。
2 只有 number
小于 5 ,组件渲染。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 function TextMemo (props ) { console .log ("子组件渲染" ); if (props) return <div > hello,world</div > ; }const controlIsRender = (pre, next ) => { if (pre.number === next.number ) { return true ; } else if (pre.number !== next.number && next.number > 5 ) { return true ; } else { return false ; } };const NewTexMemo = memo (TextMemo , controlIsRender);class Index extends React.Component { constructor (props ) { super (props); this .state = { number : 1 , num : 1 , }; } render ( ) { const { num, number } = this .state ; return ( <div > <div > 改变num:当前值 {num} <button onClick ={() => this.setState({ num: num + 1 })}> num++ </button > <button onClick ={() => this.setState({ num: num - 1 })}> num-- </button > </div > <div > 改变number: 当前值 {number} <button onClick ={() => this.setState({ number: number + 1 })} > {" "} number ++ </button > <button onClick ={() => this.setState({ number: number - 1 })} > {" "} number --{" "} </button > </div > <NewTexMemo num ={num} number ={number} /> </div > ); } }
效果:
完美达到了效果,React.memo
一定程度上,可以等价于组件外部使用shouldComponentUpdate
,用于拦截新老props
,确定组件是否更新。
forwardRef 官网对forwardRef
的概念和用法很笼统,也没有给定一个具体的案例。很多同学不知道 forwardRef
具体怎么用,下面我结合具体例子给大家讲解forwardRef
应用场景。
1 转发引入 Ref
这个场景实际很简单,比如父组件想获取孙组件,某一个dom
元素。这种隔代ref
获取引用,就需要forwardRef
来助力。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 function Son (props ) { const { grandRef } = props; return ( <div > <div > i am alien </div > <span ref ={grandRef} > 这个是想要获取元素</span > </div > ); }class Father extends React.Component { constructor (props ) { super (props); } render ( ) { return ( <div > <Son grandRef ={this.props.grandRef} /> </div > ); } }const NewFather = React .forwardRef ((props, ref ) => ( <Father grandRef ={ref} {...props } /> ));class GrandFather extends React.Component { constructor (props ) { super (props); } node = null ; componentDidMount ( ) { console .log (this .node ); } render ( ) { return ( <div > <NewFather ref ={(node) => (this.node = node)} /> </div > ); } }
效果
react
不允许ref
通过props
传递,因为组件上已经有 ref
这个属性,在组件调和过程中,已经被特殊处理,forwardRef
出现就是解决这个问题,把ref
转发到自定义的forwardRef
定义的属性上,让ref
,可以通过props
传递。
2 高阶组件转发 Ref
一文吃透hoc
文章中讲到,由于属性代理的hoc
,被包裹一层,所以如果是类组件,是通过ref
拿不到原始组件的实例的,不过我们可以通过forWardRef
转发ref
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function HOC (Component ) { class Wrap extends React.Component { render ( ) { const { forwardedRef, ...otherprops } = this .props ; return <Component ref ={forwardedRef} {...otherprops } /> ; } } return React .forwardRef ((props, ref ) => ( <Wrap forwardedRef ={ref} {...props } /> )); }class Index extends React.Component { componentDidMount ( ) { console .log (666 ); } render ( ) { return <div > hello,world</div > ; } }const HocIndex = HOC (Index , true );export default () => { const node = useRef (null ); useEffect (() => { console .log (node.current .componentDidMount ); }, []); return ( <div > <HocIndex ref ={node} /> </div > ); };
如上,解决了高阶组件引入Ref
的问题。
lazy
React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库
React.lazy
和Suspense
配合一起用,能够有动态加载组件的效果。React.lazy
接受一个函数,这个函数需要动态调用 import()
。它必须返回一个 Promise
,该 Promise
需要 resolve
一个 default export
的 React
组件。
我们模拟一个动态加载的场景。
父组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import Test from "./comTest" ;const LazyComponent = React .lazy ( () => new Promise ((resolve ) => { setTimeout (() => { resolve ({ default : () => <Test /> , }); }, 2000 ); }) );class index extends React.Component { render ( ) { return ( <div className ="context_box" style ={{ marginTop: "50px " }}> <React.Suspense fallback ={ <div className ="icon" > <SyncOutlined spin /> </div > } > <LazyComponent /> </React.Suspense > </div > ); } }
我们用setTimeout
来模拟import
异步引入效果。
Test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class Test extends React.Component { constructor (props ) { super (props); } componentDidMount ( ) { console .log ("--componentDidMount--" ); } render ( ) { return ( <div > <img src ={alien} className ="alien" /> </div > ); } }
效果
Suspense 何为Suspense
, Suspense
让组件“等待”某个异步操作,直到该异步操作结束即可渲染。
用于数据获取的 Suspense
是一个新特性,你可以使用 <Suspense>
以声明的方式来“等待”任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。
上面讲到高阶组件lazy
时候,已经用 lazy
+ Suspense
模式,构建了异步渲染组件。我们看一下官网文档中的案例:
1 2 3 4 const ProfilePage = React .lazy (() => import ("./ProfilePage" )); <Suspense fallback ={ <Spinner /> }> <ProfilePage /> </Suspense > ;
Fragment react
不允许一个组件返回多个节点元素,比如说如下情况
1 2 3 4 5 render ( ){ return <li > 🍎🍎🍎 </li > <li > 🍌🍌🍌 </li > <li > 🍇🍇🍇 </li > }
如果我们想解决这个情况,很简单,只需要在外层套一个容器元素。
1 2 3 4 5 6 7 render ( ){ return <div > <li > 🍎🍎🍎 </li > <li > 🍌🍌🍌 </li > <li > 🍇🍇🍇 </li > </div > }
但是我们不期望,增加额外的dom
节点,所以react
提供Fragment
碎片概念,能够让一个组件返回多个元素。 所以我们可以这么写
1 2 3 4 5 <React .Fragment > <li > 🍎🍎🍎 </li > <li > 🍌🍌🍌 </li > <li > 🍇🍇🍇 </li > </React .Fragment >
还可以简写成:
1 2 3 4 5 <> <li > 🍎🍎🍎 </li > <li > 🍌🍌🍌 </li > <li > 🍇🍇🍇 </li > </>
和Fragment
区别是,Fragment
可以支持key
属性。<></>
不支持key
属性。
温馨提示 。我们通过map
遍历后的元素,react
底层会处理,默认在外部嵌套一个<Fragment>
。
比如:
1 2 3 { [1 , 2 , 3 ].map ((item ) => <span key ={item.id} > {item.name}</span > ); }
react
底层处理之后,等价于:
1 2 3 4 5 <Fragment > <span > </span > <span > </span > <span > </span > </Fragment >
Profiler Profiler
这个api
一般用于开发阶段,性能检测,检测一次react
组件渲染用时,性能开销。
Profiler
需要两个参数:
第一个参数:是 id
,用于表识唯一性的Profiler
。
第二个参数:onRender
回调函数,用于渲染完成,接受渲染参数。
实践:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const index = ( ) => { const callback = (...arg ) => console .log (arg); return ( <div > <div > <Profiler id ="root" onRender ={callback} > <Router > <Meuns /> <KeepaliveRouterSwitch withoutRoute > {renderRoutes(menusList)} </KeepaliveRouterSwitch > </Router > </Profiler > </div > </div > ); };
结果
onRender
0 -id: root
-> Profiler
树的 id
。
1 -phase: mount
-> mount
挂载 , update
渲染了。
2 -actualDuration: 6.685000262223184
-> 更新 committed
花费的渲染时间。
3 -baseDuration: 4.430000321008265
-> 渲染整颗子树需要的时间
4 -startTime : 689.7299999836832
-> 本次更新开始渲染的时间
5 -commitTime : 698.5799999674782
-> 本次更新 committed 的时间
6 -interactions: set{}
-> 本次更新的 interactions
的集合
尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。
StrictMode StrictMode
见名知意,严格模式,用于检测react
项目中的潜在的问题,。与 Fragment
一样, StrictMode
不会渲染任何可见的 UI
。它为其后代元素触发额外的检查和警告。
严格模式检查仅在开发模式下运行;它们不会影响生产构建。
StrictMode
目前有助于:
① 识别不安全的生命周期。
② 关于使用过时字符串 ref API
的警告
③ 关于使用废弃的 findDOMNode
方法的警告
④ 检测意外的副作用
⑤ 检测过时的 context API
实践:识别不安全的生命周期
对于不安全的生命周期,指的是UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
, UNSAFE_componentWillUpdate
外层开启严格模式:
1 2 3 4 5 6 7 8 <React .StrictMode > <Router > <Meuns /> <KeepaliveRouterSwitch withoutRoute > {renderRoutes(menusList)} </KeepaliveRouterSwitch > </Router > </React .StrictMode >
我们在内层组件中,使用不安全的生命周期:
1 2 3 4 5 6 class Index extends React.Component { UNSAFE_componentWillReceiveProps ( ) {} render ( ) { return <div className ="box" /> ; } }
效果:
工具类 接下来我们一起来探究一下react
工具类函数的用法。
createElement 一提到createElement
,就不由得和JSX
联系一起。我们写的jsx
,最终会被 babel
,用createElement
编译成react
元素形式。我写一个组件,我们看一下会被编译成什么样子,
如果我们在render
里面这么写:
1 2 3 4 5 6 7 8 9 render ( ){ return <div className ="box" > <div className ="item" > 生命周期</div > <Text mes ="hello,world" /> <React.Fragment > Flagment </React.Fragment > { /* */ } text文本 </div > }
会被编译成这样:
1 2 3 4 5 6 7 render ( ) { return React .createElement ("div" , { className : "box" }, React .createElement ("div" , { className : "item" }, "\u751F\u547D\u5468\u671F" ), React .createElement (Text , { mes : "hello,world" }), React .createElement (React .Fragment , null , " Flagment " ), "text\u6587\u672C" ); }
当然我们可以不用jsx
模式,而是直接通过createElement
进行开发。
createElement
模型:
1 React .createElement (type, [props], [...children]);
createElement
参数:
**第一个参数:**如果是组件类型,会传入组件,如果是dom
元素类型,传入div
或者span
之类的字符串。
第二个参数: :第二个参数为一个对象,在dom
类型中为属性 ,在组件
类型中为props 。
**其他参数:**,依次为children
,根据顺序排列。
createElement 做了些什么?
经过createElement
处理,最终会形成 $$typeof = Symbol(react.element)
对象。对象上保存了该react.element
的信息。
cloneElement 可能有的同学还傻傻的分不清楚cloneElement
和createElement
区别和作用。
createElement
把我们写的jsx
,变成element
对象; 而cloneElement
的作用是以 element
元素为样板克隆并返回新的 React
元素。返回元素的 props
是将新的 props
与原始元素的 props
浅层合并后的结果。
那么cloneElement
感觉在我们实际业务组件中,可能没什么用,但是在一些开源项目,或者是公共插槽组件中 用处还是蛮大的,比如说,我们可以在组件中,劫持children element
,然后通过cloneElement
克隆element
,混入props
。经典的案例就是 react-router
中的Swtich
组件,通过这种方式,来匹配唯一的 Route
并加以渲染。
我们设置一个场景,在组件中,去劫持children
,然后给children
赋能一些额外的props
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function FatherComponent ({ children } ) { const newChildren = React .cloneElement (children, { age : 18 }); return <div > {newChildren} </div > ; }function SonComponent (props ) { console .log (props); return <div > hello,world</div > ; }class Index extends React.Component { render ( ) { return ( <div className ="box" > <FatherComponent > <SonComponent name ="alien" /> </FatherComponent > </div > ); } }
打印:
完美达到了效果!
createContext createContext
用于创建一个Context
对象,createContext
对象中,包括用于传递 Context
对象值 value
的Provider
,和接受value
变化订阅的Consumer
。
1 const MyContext = React .createContext (defaultValue);
createContext
接受一个参数defaultValue
,如果Consumer
上一级一直没有Provider
,则会应用defaultValue
作为value
。只有 当组件所处的树中没有匹配到 Provider
时,其 defaultValue
参数才会生效。
我们来模拟一个 Context.Provider
和Context.Consumer
的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 function ComponentB ( ) { return ( <MyContext.Consumer > {(value) => <ComponentA {...value } /> } </MyContext.Consumer > ); }function ComponentA (props ) { const { name, mes } = props; return ( <div > <div > 姓名: {name} </div > <div > 想对大家说: {mes} </div > </div > ); }function index ( ) { const [value] = React .useState ({ name : "alien" , mes : "let us learn React " , }); return ( <div style ={{ marginTop: "50px " }}> <MyContext.Provider value ={value} > <ComponentB /> </MyContext.Provider > </div > ); }
打印结果:
Provider
和Consumer
的良好的特性,可以做数据的存 和取 ,Consumer
一方面传递value
,另一方面可以订阅value
的改变。
Provider
还有一个特性可以层层传递value
,这种特性在react-redux
中表现的淋漓尽致。
createFactory 1 React .createFactory (type);
返回用于生成指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 ‘div
‘ 或 ‘span
‘),也可以是 React 组件 类型 ( class
组件或函数组件),或是 React fragment
类型。
使用:
1 2 3 4 5 6 7 8 const Text = React .createFactory (() => <div > hello,world</div > );function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <Text /> </div > ); }
效果
报出警告,这个api
将要被废弃,我们这里就不多讲了,如果想要达到同样的效果,请用React.createElement
createRef createRef
可以创建一个 ref
元素,附加在react
元素上。
用法:
1 2 3 4 5 6 7 8 9 10 11 12 class Index extends React.Component { constructor (props ) { super (props); this .node = React .createRef (); } componentDidMount ( ) { console .log (this .node ); } render ( ) { return <div ref ={this.node} > my name is alien </div > ; } }
个人觉得createRef
这个方法,很鸡肋,我们完全可以class
类组件中这么写,来捕获ref
。
1 2 3 4 5 6 7 8 9 class Index extends React.Component { node = null ; componentDidMount ( ) { console .log (this .node ); } render ( ) { return <div ref ={(node) => this.node}> my name is alien </div > ; } }
或者在function
组件中这么写:
1 2 3 4 5 6 7 function Index ( ) { const node = React .useRef (null ); useEffect (() => { console .log (node.current ); }, []); return <div ref ={node} > my name is alien </div > ; }
isValidElement 这个方法可以用来检测是否为react element
元素,接受待验证对象,返回true
或者false
。这个 api 可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是react element
元素。 但是,对于一起公共组件或是开源库,isValidElement
就很有作用了。
实践
我们做一个场景,验证容器组件的所有子组件,过滤到非react element
类型。
没有用isValidElement
验证之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const Text = ( ) => <div > hello,world</div > ;class WarpComponent extends React.Component { constructor (props ) { super (props); } render ( ) { return this .props .children ; } }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > <Text /> <div > my name is alien </div > Let's learn react together! </WarpComponent > </div > ); }
过滤之前的效果
我们用isValidElement
进行react element
验证:
1 2 3 4 5 6 7 8 9 10 11 class WarpComponent extends React.Component { constructor (props ) { super (props); this .newChidren = this .props .children .filter ((item ) => React .isValidElement (item) ); } render ( ) { return this .newChidren ; } }
过滤之后效果
过滤掉了非react element
的 Let's learn react together!
。
Children.map 接下来的五个api
都是和react.Chidren
相关的,我们来分别介绍一下,我们先来看看官网的描述,React.Children
提供了用于处理 this.props.children
不透明数据结构的实用方法。
有的同学会问遍历 children
用数组方法,map
,forEach
不就可以了吗? 请我们注意一下不透明数据结构
,什么叫做不透明结构?
我们先看一下透明的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Text extends React.Component { render ( ) { return <div > hello,world</div > ; } }function WarpComponent (props ) { console .log (props.children ); return props.children ; }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > <Text /> <Text /> <Text /> <span > hello,world</span > </WarpComponent > </div > ); }
打印
但是我们把Index
结构改变一下:
1 2 3 4 5 6 7 8 9 10 11 12 function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > {new Array(3).fill(0).map(() => ( <Text /> ))} <span > hello,world</span > </WarpComponent > </div > ); }
打印
这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren
来帮忙了。
但是我们把WarpComponent
组件用react.Chidren
处理children
:
1 2 3 4 5 function WarpComponent (props ) { const newChildren = React .Children .map (props.children , (item ) => item); console .log (newChildren); return newChildren; }
此时就能正常遍历了,达到了预期效果。
注意 如果 children
是一个 Fragment
对象,它将被视为单一子节点的情况处理,而不会被遍历。
Children.forEach Children.forEach
和Children.map
用法类似,Children.map
可以返回新的数组,Children.forEach
仅停留在遍历阶段。
我们将上面的WarpComponent
方法,用Children.forEach
改一下。
1 2 3 4 function WarpComponent (props ) { React .Children .forEach (props.children , (item ) => console .log (item)); return props.children ; }
Children.count children
中的组件总数量,等同于通过 map
或 forEach
调用回调函数的次数。对于更复杂的结果,Children.count
可以返回同一级别子组件的数量。
我们还是把上述例子进行改造:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function WarpComponent (props ) { const childrenCount = React .Children .count (props.children ); console .log (childrenCount, "childrenCount" ); return props.children ; }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > {new Array(3) .fill(0) .map((item, index) => new Array(2) .fill(1) .map((item, index1) => ( <Text key ={index + index1 } /> )) )} <span > hello,world</span > </WarpComponent > </div > ); }
效果:
Children.toArray Children.toArray
返回,props.children
扁平化后结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function WarpComponent (props ) { const newChidrenArray = React .Children .toArray (props.children ); console .log (newChidrenArray, "newChidrenArray" ); return newChidrenArray; }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > {new Array(3) .fill(0) .map((item, index) => new Array(2) .fill(1) .map((item, index1) => ( <Text key ={index + index1 } /> )) )} <span > hello,world</span > </WarpComponent > </div > ); }
效果:
newChidrenArray ,就是扁平化的数组结构。React.Children.toArray()
在拉平展开子节点列表时,更改 key
值以保留嵌套数组的语义。也就是说, toArray
会为返回数组中的每个 key
添加前缀,以使得每个元素 key
的范围都限定在此函数入参数组的对象内。
Children.only 验证 children
是否只有一个子节点(一个 React
元素),如果有则返回它,否则此方法会抛出错误。
不唯一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function WarpComponent (props ) { console .log (React .Children .only (props.children )); return props.children ; }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > {new Array(3).fill(0).map((item, index) => ( <Text key ={index} /> ))} <span > hello,world</span > </WarpComponent > </div > ); }
效果
唯一
1 2 3 4 5 6 7 8 9 10 11 12 13 function WarpComponent (props ) { console .log (React .Children .only (props.children )); return props.children ; }function Index ( ) { return ( <div style ={{ marginTop: "50px " }}> <WarpComponent > <Text /> </WarpComponent > </div > ); }
效果
React.Children.only()
不接受 React.Children.map()
的返回值,因为它是一个数组而并不是 React
元素。
react-hooks 对于react-hooks
,我已经写了三部曲,介绍了react-hooks
使用,自定义hooks
,以及react-hooks
原理,感兴趣的同学可以去看看,文章末尾有链接,对于常用的api
,我这里参考了react-hooks
如何使用那篇文章。并做了相应精简化和一些内容的补充。
useState useState
可以弥补函数组件没有state
的缺陷。useState
可以接受一个初识值,也可以是一个函数action
,action
返回值作为新的state
。返回一个数组,第一个值为state
读取值,第二个值为改变state
的dispatchAction
函数。
我们看一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const DemoState = (props ) => { let [number, setNumber] = useState (0 ); return ( <div > <span > {number}</span > <button onClick ={() => { setNumber(number + 1); /* 写法一 */ setNumber((number) => number + 1); /* 写法二 */ console.log(number); /* 这里的number是不能够即时改变的 */ }} > num++ </button > </div > ); };
useEffect useEffect
可以弥补函数组件没有生命周期的缺点。我们可以在useEffect
第一个参数回调函数中,做一些请求数据,事件监听等操作,第二个参数作为dep
依赖项,当依赖项发生变化,重新执行第一个函数。
useEffect 可以用作数据交互。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 function getUserInfo (a ) { return new Promise ((resolve ) => { setTimeout (() => { resolve ({ name : a, age : 16 , }); }, 500 ); }); }const DemoEffect = ({ a } ) => { const [userMessage, setUserMessage]: any = useState ({}); const div = useRef (); const [number, setNumber] = useState (0 ); const handleResize = ( ) => {}; useEffect (() => { getUserInfo (a).then ((res ) => { setUserMessage (res); }); console .log (div.current ); window .addEventListener ("resize" , handleResize); }, [a, number]); return ( <div ref ={div} > <span > {userMessage.name}</span > <span > {userMessage.age}</span > <div onClick ={() => setNumber(1)}>{number}</div > </div > ); };
useEffect 可以用作事件监听,还有一些基于dom
的操作。 ,别忘了在useEffect
第一个参数回调函数,返一个函数用于清除事件监听等操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const DemoEffect = ({ a } ) => { const handleResize = ( ) => {}; useEffect (() => { const timer = setInterval (() => console .log (666 ), 1000 ); window .addEventListener ("resize" , handleResize); return function ( ) { clearInterval (timer); window .removeEventListener ("resize" , handleResize); }; }, [a]); return <div > </div > ; };
useMemo useMemo
接受两个参数,第一个参数是一个函数,返回值用于产生保存值 。 第二个参数是一个数组,作为dep
依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值 。
应用场景: 1 缓存一些值,避免重新执行上下文
1 2 3 4 const number = useMemo (() => { return number; }, [props.number ]);
2 减少不必要的dom
循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { useMemo ( () => ( <div > {selectList.map((i, v) => ( <span className ={style.listSpan} key ={v} > {i.patentName} </span > ))} </div > ), [selectList] ); }
3 减少子组件渲染
1 2 3 4 5 const goodListChild = useMemo ( () => <GoodList list ={props.list} /> , [props.list ] );
useCallback useMemo
和 useCallback
接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo
返回的是函数运行的结果, useCallback
返回的是函数。 返回的callback
可以作为props
回调函数传递给子组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const DemoChildren = React .memo ((props ) => { console .log ("子组件更新" ); useEffect (() => { props.getInfo ("子组件" ); }, []); return <div > 子组件</div > ; });const DemoUseCallback = ({ id } ) => { const [number, setNumber] = useState (1 ); const getInfo = useCallback ( (sonName ) => { console .log (sonName); }, [id] ); return ( <div > {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */} <button onClick ={() => setNumber(number + 1)}>增加</button > <DemoChildren getInfo ={getInfo} /> </div > ); };
useRef useRef
的作用:
一 是可以用来获取dom
元素,或者class
组件实例 。
二 react-hooks原理
文章中讲过,创建useRef
时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以利用这个特性,来通过useRef
保存一些数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const DemoUseRef = ( ) => { const dom = useRef (null ); const handerSubmit = ( ) => { console .log (dom.current ); }; return ( <div > {/* ref 标记当前dom节点 */} <div ref ={dom} > 表单组件</div > <button onClick ={() => handerSubmit()}>提交</button > </div > ); };
useLayoutEffect useEffect
执行顺序: 组件更新挂载完成 -> 浏览器 dom
绘制完成 -> 执行 useEffect
回调。 useLayoutEffect
执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect
回调-> 浏览器dom
绘制完成。
所以说 useLayoutEffect
代码可能会阻塞浏览器的绘制 。我们写的 effect
和 useLayoutEffect
,react
在底层会被分别打上PassiveEffect
,HookLayout
,在commit
阶段区分出,在什么时机执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 const DemoUseLayoutEffect = ( ) => { const target = useRef (); useLayoutEffect (() => { const { x, y } = getPositon (); animate (target.current , { x, y }); }, []); return ( <div > <span ref ={target} className ="animate" > </span > </div > ); };
useReducer 在react-hooks
原理那篇文章中讲解到,useState
底层就是一个简单版的useReducer
useReducer
接受的第一个参数是一个函数,我们可以认为它就是一个 reducer
, reducer
的参数就是常规 reducer
里面的 state
和 action
,返回改变后的 state
, useReducer
第二个参数为 state
的初始值 返回一个数组,数组的第一项就是更新之后 state
的值 ,第二个参数是派发更新的 dispatch
函数。
我们来看一下useReducer
如何使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const DemoUseReducer = ( ) => { const [number, dispatchNumbner] = useReducer ((state, action ) => { const { payload, name } = action; switch (name) { case "add" : return state + 1 ; case "sub" : return state - 1 ; case "reset" : return payload; } return state; }, 0 ); return ( <div > 当前值:{number} {/* 派发更新 */} <button onClick ={() => dispatchNumbner({ name: "add" })}> 增加 </button > <button onClick ={() => dispatchNumbner({ name: "sub" })}> 减少 </button > <button onClick ={() => dispatchNumbner({ name: "reset", payload: 666 })} > 赋值 </button > {/* 把dispatch 和 state 传递给子组件 */} <MyChildren dispatch ={dispatchNumbner} State ={{ number }} /> </div > ); };
useContext 我们可以使用 useContext
,来获取父级组件传递过来的 context
值,这个当前值就是最近的父级组件 Provider
设置的 value
值,useContext
参数一般是由 createContext
方式引入 ,也可以父级上下文 context
传递 ( 参数为 context
)。useContext
可以代替 context.Consumer
来获取 Provider
中保存的 value
值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 const DemoContext = ( ) => { const value : any = useContext (Context ); return <div > my name is {value.name}</div > ; };const DemoContext1 = ( ) => { return ( <Context.Consumer > {/* my name is alien */} {(value) => <div > my name is {value.name}</div > } </Context.Consumer > ); };export default () => { return ( <div > <Context.Provider value ={{ name: "alien ", age: 18 }}> <DemoContext /> <DemoContext1 /> </Context.Provider > </div > ); };
useImperativeHandle useImperativeHandle
可以配合 forwardRef
自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是class
类组件,我们可以通过ref
获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref
的,那么此时useImperativeHandle
和 forwardRef
配合就能达到效果。
useImperativeHandle
接受三个参数:
第一个参数 ref: 接受 forWardRef
传递过来的 ref
。
第二个参数 createHandle
:处理函数,返回值作为暴露给父组件的ref
对象。
第三个参数 deps
:依赖项 deps
,依赖项更改形成新的ref
对象。
我们来模拟给场景,用useImperativeHandle
,使得父组件能让子组件中的input
自动赋值并聚焦。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 function Son (props, ref ) { console .log (props); const inputRef = useRef (null ); const [inputValue, setInputValue] = useState ("" ); useImperativeHandle ( ref, () => { const handleRefs = { onFocus ( ) { inputRef.current .focus (); }, onChangeValue (value ) { setInputValue (value); }, }; return handleRefs; }, [] ); return ( <div > <input placeholder ="请输入内容" ref ={inputRef} value ={inputValue} /> </div > ); }const ForwarSon = forwardRef (Son );class Index extends React.Component { inputRef = null ; handerClick ( ) { const { onFocus, onChangeValue } = this .cur ; onFocus (); onChangeValue ("let us learn React!" ); } render ( ) { return ( <div style ={{ marginTop: "50px " }}> <ForwarSon ref ={(node) => (this.inputRef = node)} /> <button onClick ={this.handerClick.bind(this)} > 操控子组件 </button > </div > ); } }
效果:
useDebugValue useDebugValue
可用于在 React
开发者工具中显示自定义 hook
的标签。这个hooks
目的就是检查自定义hooks
1 2 3 4 5 6 7 8 9 function useFriendStatus (friendID ) { const [isOnline, setIsOnline] = useState (null ); useDebugValue (isOnline ? "Online" : "Offline" ); return isOnline; }
我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。
useTransition useTransition
允许延时由state
改变而带来的视图渲染。避免不必要的渲染。它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新。
1 2 const TIMEOUT_MS = { timeoutMs : 2000 };const [startTransition, isPending] = useTransition (TIMEOUT_MS );
useTransition
接受一个对象, timeoutMs
代码需要延时的时间。
返回一个数组。第一个参数: 是一个接受回调的函数。我们用它来告诉 React
需要推迟的 state
。 第二个参数: 一个布尔值。表示是否正在等待,过度状态的完成(延时state
的更新)。
下面我们引入官网的列子,来了解useTransition
的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const SUSPENSE_CONFIG = { timeoutMs : 2000 };function App ( ) { const [resource, setResource] = useState (initialResource); const [startTransition, isPending] = useTransition (SUSPENSE_CONFIG ); return ( <> <button disabled ={isPending} onClick ={() => { startTransition(() => { const nextUserId = getNextId(resource.userId); setResource(fetchProfileData(nextUserId)); }); }} > Next </button > {isPending ? " 加载中..." : null} <Suspense fallback ={ <Spinner /> }> <ProfilePage resource ={resource} /> </Suspense > </> ); }
在这段代码中,我们使用 startTransition
包装了我们的数据获取。这使我们可以立即开始获取用户资料的数据,同时推迟下一个用户资料页面以及其关联的 Spinner
的渲染 2 秒钟( timeoutMs
中显示的时间)。
这个api
目前处于实验阶段,没有被完全开放出来。
react-dom 接下来,我们来一起研究react-dom
中比较重要的api
。
render render
是我们最常用的react-dom
的 api
,用于渲染一个react
元素,一般react
项目我们都用它,渲染根部容器app
。
1 ReactDOM .render (element, container[, callback])
使用
1 ReactDOM .render (<App /> , document .getElementById ("app" ));
ReactDOM.render
会控制container
容器节点里的内容,但是不会修改容器节点本身。
hydrate 服务端渲染用hydrate
。用法与 render()
相同,但它用于在 ReactDOMServer
渲染的容器中对 HTML
的内容进行 hydrate
操作。
1 ReactDOM .hydrate (element, container[, callback])
createPortal Portal
提供了一种将子节点渲染到存在于父组件以外的 DOM
节点的优秀的方案。createPortal
可以把当前组件或 element
元素的子节点,渲染到组件之外的其他地方。
那么具体应用到什么场景呢?
比如一些全局的弹窗组件model
,<Model/>
组件一般都写在我们的组件内部,倒是真正挂载的dom
,都是在外层容器,比如body
上。此时就很适合createPortal
API。
createPortal
接受两个参数:
1 ReactDOM .createPortal (child, container);
第一个: child
是任何可渲染的 React
子元素 第二个: container
是一个 DOM
元素。
接下来我们实践一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function WrapComponent ({ children } ) { const domRef = useRef (null ); const [PortalComponent , setPortalComponent] = useState (null ); React .useEffect (() => { setPortalComponent (ReactDOM .createPortal (children, domRef.current )); }, []); return ( <div > <div className ="container" ref ={domRef} > </div > {PortalComponent} </div > ); }class Index extends React.Component { render ( ) { return ( <div style ={{ marginTop: "50px " }}> <WrapComponent > <div > hello,world</div > </WrapComponent > </div > ); } }
效果
我们可以看到,我们children
实际在container
之外挂载的,但是已经被createPortal
渲染到container
中。
unstable_batchedUpdates 在react-legacy
模式下,对于事件,react
事件有批量更新来处理功能,但是这一些非常规的事件中,批量更新功能会被打破。所以我们可以用react-dom
中提供的unstable_batchedUpdates
来进行批量更新。
一次点击实现的批量更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Index extends React.Component { constructor (props ) { super (props); this .state = { numer : 1 , }; } handerClick = () => { this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); }; render ( ) { return ( <div style ={{ marginTop: "50px " }}> <button onClick ={this.handerClick} > click me</button > </div > ); } }
效果
渲染次数一次。
批量更新条件被打破
1 2 3 4 5 6 7 8 9 10 handerClick = () => { Promise .resolve ().then (() => { this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); }); };
效果
渲染次数三次。
unstable_batchedUpdate 助力
1 2 3 4 5 6 7 8 9 10 11 12 handerClick = () => { Promise .resolve ().then (() => { ReactDOM .unstable_batchedUpdates (() => { this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); this .setState ({ numer : this .state .numer + 1 }); console .log (this .state .numer ); }); }); };
渲染次数一次,完美解决批量更新问题。
flushSync flushSync
可以将回调函数中的更新任务,放在一个较高的优先级中。我们知道react
设定了很多不同优先级的更新任务。如果一次更新任务在flushSync
回调函数内部,那么将获得一个较高优先级的更新。比如
1 2 3 4 ReactDOM .flushSync (() => { this .setState ({ name : "alien" }); });
为了让大家理解flushSync
,我这里做一个demo
奉上,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import ReactDOM from "react-dom" ;class Index extends React.Component { state = { number : 0 }; handerClick = () => { setTimeout (() => { this .setState ({ number : 1 }); }); this .setState ({ number : 2 }); ReactDOM .flushSync (() => { this .setState ({ number : 3 }); }); this .setState ({ number : 4 }); }; render ( ) { const { number } = this .state ; console .log (number); return ( <div > <div > {number}</div > <button onClick ={this.handerClick} > 测试flushSync</button > </div > ); } }
先不看答案,点击一下按钮,打印什么呢?
我们来点击一下看看
打印 0 3 4 1 ,相信不难理解为什么这么打印了。
首先 flushSync
this.setState({ number: 3 })
设定了一个高优先级的更新,所以 3 先被打印
2 4 被批量更新为 4
相信这个demo
让我们更深入了解了flushSync
。
findDOMNode findDOMNode
用于访问组件DOM
元素节点,react
推荐使用ref
模式,不期望使用findDOMNode
。
1 ReactDOM .findDOMNode (component);
注意的是:
1 findDOMNode
只能用在已经挂载的组件上。
2 如果组件渲染内容为 null
或者是 false
,那么 findDOMNode
返回值也是 null
。
3 findDOMNode
不能用于函数组件。
接下来让我们看一下,findDOMNode
具体怎么使用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Index extends React.Component { handerFindDom = () => { console .log (ReactDOM .findDOMNode (this )); }; render ( ) { return ( <div style ={{ marginTop: "100px " }}> <div > hello,world</div > <button onClick ={this.handerFindDom} > 获取容器dom</button > </div > ); } }
效果:
我们完全可以将外层容器用ref
来标记,获取捕获原生的dom
节点。
unmountComponentAtNode 从 DOM
中卸载组件,会将其事件处理器和 state
一并清除。 如果指定容器上没有对应已挂载的组件,这个函数什么也不会做。如果组件被移除将会返回 true
,如果没有组件可被移除将会返回 false
。
我们来简单举例看看unmountComponentAtNode
如何使用?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function Text ( ) { return <div > hello,world</div > ; }class Index extends React.Component { node = null ; constructor (props ) { super (props); this .state = { numer : 1 , }; } componentDidMount ( ) { ReactDOM .render (<Text /> , this .node ); } handerClick = () => { const state = ReactDOM .unmountComponentAtNode (this .node ); console .log (state); }; render ( ) { return ( <div style ={{ marginTop: "50px " }}> <div ref ={(node) => (this.node = node)}></div > <button onClick ={this.handerClick} > click me</button > </div > ); } }
效果
总结 本文通过react
组件层面,工具层面,hooks
层面,react-dom
了解了api
的用法,希望看完的同学,能够对着文章中的demo
自己敲一遍,到头来会发现自己成长不少。
最后, 送人玫瑰,手留余香,觉得有收获的朋友可以给笔者点赞,关注 一波 ,陆续更新前端超硬核文章。
提前透漏:接下来会出一部揭秘react
事件系统的文章。
感兴趣的同学请关注公众号 前端Sharing
持续推送优质好文
往期 react 文章 文章中,对于其他没有讲到的react-hooks
,建议大家看react-hooks
三部曲。
react-hooks 三部曲
react 进阶系列
react 源码系列
开源项目系列
参考文档 react 中文文档